Starting with ASP.NET 3.5, you can use Windows
Communication Foundation to build AJAX-callable services. Overall, the
developer’s experience is mostly the same whether you use ASP.NET Web
services or WCF services. Instead, the richness of the WCF
platform—specifically designed to generate and support software
services—is a no-brainer. A good question, then, is, “Why weren’t WCF
services available in ASP.NET AJAX pages before ASP.NET 3.5?”
Before the availability of
the .NET Framework 3.5, the WCF platform had no built-in support for
taking JSON as input and returning it as output. So what’s does the .NET
Framework 3.5 really do in the area of WCF? It basically empowers WCF
to support JSON serialization. Now WCF services can optionally output
JSON, and not always SOAP envelopes, over HTTP. All that you have to do
is configure an endpoint to use the webHttpBinding binding model and enable Web scripting through a new attribute. I’ll provide more detail about this in a moment.
Building a Simple WCF Service
Having always been a huge
fan of the bottom-up approach to things, I just can’t learn anything
new without first testing it in a very simple scenario that then evolves
as quickly as possible into a more realistic one. So let’s simply
create a new Web site in Visual Studio 2008, click to add a new WCF
service item, and name the item TimeService.
Rewriting TimeService as a WCF Service
After confirming the operation, you find your project extended with a service endpoint (say, timeservice.svc) and its related code-behind file placed in the App_Code folder—say, wcftimeservice.cs. In addition, the web.config file is modified, too, to provide registration and discovery information for the service being created.
You might want to
define the contract for the service by using an interface. This is not
strictly required, but it is helpful and also gives you the possibility
of implementing multiple contracts in the same actual class.
namespace Core35.Services.Wcf
{
// Contract
[ServiceContract(Namespace = "Core35.Services", Name="WcfTimeService")]
public interface ITimeService
{
[OperationContract]
DateTime GetTime();
[OperationContract]
string GetTimeFormat(string format);
}
}
In the example, the ITimeService interface represents the contract of the service. The ServiceContract attribute marks the contract, whereas the OperationContract
attribute indicates methods. In simpler cases, you can just define a
class that is both the contract and implementation. If you do so, you
use the ServiceContract and OperationContract attributes directly in the class.
Pay attention to the Namespace and Name properties of the ServiceContract
attribute. They gain additional importance in AJAX-enabled WCF
services, as we’ll see in a moment. The following code shows a class
that implements the ITimeService contract:
using System;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.ServiceModel.Activation;
using System.ServiceModel.Web;
namespace Core35.Services.Wcf
{
[AspNetCompatibilityRequirements(
RequirementsMode=AspNetCompatibilityRequirementsMode.Allowed)]
public class TimeService : ITimeService
{
public DateTime GetTime()
{
return DateTime.Now;
}
public string GetTimeFormat(string format)
{
return DateTime.Now.ToString(format);
}
}
}
In the end, the TimeService class exposes a couple of public endpoints—named GetTime and GetTimeFormat.
Registering the Service
The endpoint to reach the methods on this interface are defined in an SVC file, like the timeservice.svc file shown next:
<%@ ServiceHost Debug="true"
Service="Core35.Services.Wcf.TimeService"
CodeBehind="~/App_Code/WcfTimeService.cs" %>
The service host,
whether it is running in debug or release mode, indicates the location
of the source files and the type that implements the service. If the
code for the service is placed inline in the SVC file, you must indicate
an additional Language attribute.
The final step before you can test the service is registering its usage in the web.config file of the host ASP.NET application. Here’s what you need to have:
<system.serviceModel>
<behaviors>
<endpointBehaviors>
<behavior name="TimeServiceAspNetAjaxBehavior">
<enableWebScript />
</behavior>
</endpointBehaviors>
</behaviors>
<serviceHostingEnvironment aspNetCompatibilityEnabled="true" />
<services>
<service name="Core35.Services.Wcf.TimeService">
<endpoint address=""
behaviorConfiguration="TimeServiceAspNetAjaxBehavior"
binding="webHttpBinding"
contract="Core35.Services.Wcf.ITimeService" />
</service>
</services>
</system.serviceModel>
First, you register the list of behaviors for endpoints. In doing so, you define a behavior for your service—named TimeServiceAspNetAjaxBehavior—and state that it accepts requests from the Web via script. The enableWebScript element is logically equivalent to the ScriptService attribute you use to decorate a Web service class for the same purpose.
Next, you list the services hosted by the current ASP.NET application. This preceding web.config file has just one service coded in the class Core35.Services.Wcf.TimeService with one endpoint using the ITimeService contract and the webHttpBinding binding model. The name attribute must be set to the type of the class implementing the service.
Testing the Service
The service is pretty much all set. How would you use it from the <script>
section of a client ASP.NET page? The steps required from a developer
aren’t much different from those required to invoke a Web service. You
start by registering the service with the script manager using the SVC
endpoint:
<asp:ScriptManager ID="ScriptManager1" runat="server">
<Services>
<asp:ServiceReference Path="~/TimeService.svc" />
</Services>
</asp:ScriptManager>
When processing the markup, the ScriptManager
control triggers additional requests to generate and download the
JavaScript proxy class for the specified WCF service. The client page
uses the proxy class to place calls.
The proxy class is named after the Namespace and Name properties of the ServiceContract attribute of the contract. If you leave the parameters to their default values, the JavaScript proxy class is named Tempuri.org.ITimeService, where Tempuri.org is the default namespace and the interface name is the default name of the contract.
There’s no relationship
between the name of the service class and JavaScript proxy class, not
even when you use the same class to provide both the contract and
implementation of the service. The name of the JavaScript proxy class
depends on namespace and name of the service contract. It is common to
assign the namespace a unique URI, such as http://www.Core35-Book.com. In this case, the name of the proxy class comes out a bit mangled, like http://www.Core35Book.com.
It is recommended, therefore, that you use plain strings to name the
namespace of a contract being used in an AJAX-enabled WCF service.
Let’s assume the following, instead:
[ServiceContract(Namespace = "Core35.Services", Name="WcfTimeService")]
In this case, the following JavaScript can be used to invoke the method GetTimeFormat:
<script language="javascript" type="text/javascript">
function getTime()
{
Core35.Services.WcfTimeService.GetTimeFormat(
"dd-mm-yyyy [hh:mm:ss]", onMethodCompleted);
}
function onMethodCompleted(results)
{
$get("lblCurrentTime").innerText = results;
}
</script>
<form id="Form1" runat="server">
<asp:ScriptManager ID="ScriptManager1" runat="server">
<Services>
<asp:ServiceReference Path="~/Services/TimeService.svc" />
</Services>
</asp:ScriptManager>
<h1>What time is on the server? Set your clock...</h1>
<input type="button" value="Get time" onclick="getTime()" />
<hr />
<asp:Label runat="server" ID="lblCurrentTime" />
</form>
The
JavaScript proxy class is made of static methods whose name and
signature match the prototype of the WCF service endpoints. In addition,
and like the ASP.NET AJAX Web services, each JavaScript proxy method
supports a bunch of additional parameters—callback functions to handle
the success or failure of the operation.
ASP.NET Compatibility Modes
When you create a new WCF service for ASP.NET AJAX, the service class is also decorated by default by the AspNetCompatibilityRequirements attribute, which deserves a few words of its own.
[AspNetCompatibilityRequirements(
RequirementsMode=AspNetCompatibilityRequirementsMode.Allowed)]
public class TimeService : ITimeService
{
:
}
Although they are
designed to be transport independent, when WCF services are employed in
the context of an ASP.NET AJAX application, they might actually work in a
manner very similar to ASMX services. By using the AspNetCompatibilityRequirements
attribute, you state your preference of having WCF and ASMX services
work according to the same model. One practical repercussion of this
setting is that when a WCF service is activated, the runtime checks
declared endpoints and ensures that all of them use the Web HTTP binding
model.
Their compatibility with ASMX services enables WCF services to access, for example, the HttpContext
object and subsequently other ASP.NET intrinsic objects. The
compatibility is required at two levels. The first level is in the web.config file, where you use the following:
<system.serviceModel>
:
<serviceHostingEnvironment aspNetCompatibilityEnabled="true" />
</system.serviceModel>
Second, developers need to explicitly choose the compatibility mode for a given WCF service by using the service AspNetCompatibilityRequirements attribute.
Building a Less Simple Service
The
extender calls into a service to receive an array of suggestions. The
service can be either a scriptable Web service or an AJAX-enabled WCF
service. Let’s see what it takes to create a helper WCF service for the AutoComplete extender.
The Suggestions Service
A service for the
auto-complete extender can have any number of operations, but all have
the same prototype. Here’s a possible contract:
[ServiceContract(Namespace = "Core35.Services", Name = "SuggestionService")]
public interface ISuggestionService
{
[OperationContract]
string[] GetCustomerNames(string prefixText, int count);
[OperationContract]
string[] GetCustomerIDs(string prefixText, int count);
}
The implementation
contains the code to query for customer names and ID and, optionally,
for some server-side caching. Here’s a fragment of the service class:
[AspNetCompatibilityRequirements(
RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
public class SuggestionService : ISuggestionService
{
public string[] GetCustomerIDs(string prefixText, int count)
{
int i=0;
DataView data = GetData();
data = FilterDataByID(data, prefixText);
string [] suggestions = new string[data.Count];
foreach (DataRowView row in data) {
suggestions[i++] = row["customerID"].ToString();
}
return suggestions;
}
// Other methods here
...
}
The next step is creating the endpoint for the service. Let’s call it suggestions.svc:
<%@ ServiceHost
Service="Core35.Services.Wcf.SuggestionService"
CodeBehind="~/App_Code/WcfSuggestionService.cs" %>
At this point, you link the auto-complete extender to the WCF service, as shown next:
<act:AutoCompleteExtender runat="server" ID="AutoCompleteExtender1"
...
ServicePath="~/Services/Suggestions.svc"
ServiceMethod="GetCustomerIDs" />
One more step is
left—publishing the contract in a service host. For an AJAX-enabled WCF
service, the host is Microsoft Internet Information Services (IIS).
However, you still need to publish the endpoint.
Service Without Configuration
Publishing a given contract means binding the contract to a public endpoint. This usually requires adding a new <service> block in the web.config file under the <services> section, as shown here:
<services>
<service name="Core35.Services.Wcf.SuggestionService">
<endpoint behaviorConfiguration="StandardServiceAspNetAjaxBehavior"
binding="webHttpBinding"
contract="Core35.Services.Wcf.ISuggestionService" />
</service>
</services>
The key thing to notice
is that AJAX-enabled WCF services can also be deployed without
configuration. All that you have to do is add a new Factory attribute to the @ServiceHost directive in the SVC file:
<%@ ServiceHost
Factory="System.ServiceModel.Activation.WebScriptServiceHostFactory"
Service="Core35.Services.Wcf.SuggestionService"
CodeBehind="~/App_Code/WcfSuggestionService.cs" %>
By taking this approach, you do not need to make changes in the web.config file, and creating a WCF service for AJAX pages is as easy as creating the class and defining the endpoint.
Data Contracts
Any nonprimitive data to be sent or received over WCF methods must be marked with the DataContract attribute. Imagine you have the following service:
[ServiceContract(Namespace = "Core35.Services.Wcf")]
[AspNetCompatibilityRequirements(
RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
public class CustomerService
{
[OperationContract]
public CustomerDTO LookupCustomer(string id)
{
NorthwindDataContext context = new NorthwindDataContext();
var data = (from c in context.Customers
where c.CustomerID == id
select c).SingleOrDefault();
if (data != null)
{
CustomerDTO dto = new CustomerDTO((Customer)data);
return dto;
}
else
return null;
}
}
The method LookupCustomer is expected to return a custom object. This object must be decorated with ad hoc DataContract attributes:
namespace Core35.Services.Wcf
{
[DataContract]
public class CustomerDTO
{
private Customer _c;
public CustomerDTO(Customer c)
{
this._c = c;
}
[DataMember]
public string CustomerID
{
get { return _c.CustomerID; }
set { _c.CustomerID = value; }
}
...
}
}
In this particular
case, the class being used over WCF is a data-transfer object (DTO)—that
is, a helper class that moves the content of domain model objects
across tiers.
Note
Could you directly use the Customer class obtained from Linq-to-SQL? Yes, as long as the class and its members are flagged with the DataContract and DataMember
attributes. Linqto-SQL classes as generated by the Visual Studio 2008
O/R designer are kind of anemic objects (only data, no behavior) and, as
such, they are just perfect as DTOs. However, you need to make sure
that they contain the right DataContract and DataMember attributes. You can add these attributes automatically by setting the SerializationMode property of the data context class to Unidirectional.
|